﻿@using Berp;
@helper CallProduction(ProductionRule production)
{
  switch(production.Type)
  {
    case ProductionRuleType.Start:
      @:this.startRule(context, RuleType.@production.RuleName);
      break;
    case ProductionRuleType.End:
      @:this.endRule(context);
      break;
    case ProductionRuleType.Process:
      @:this.build(context, token);
      break;
  }
}
@helper HandleParserError(IEnumerable<string> expectedTokens, State state)
{<text>
    token.detach();
    const expectedTokens = ["@Raw(string.Join("\", \"", expectedTokens))"];
    const error = token.isEof ?
      UnexpectedEOFException.create(token, expectedTokens) :
      UnexpectedTokenException.create(token, expectedTokens);
    if (this.stopAtFirstError) throw error;
    this.addError(context, error);
    return @state.Id;</text>}
@helper MatchToken(TokenType tokenType)
{<text>match_@(tokenType)(context, token)</text>}
// This file is generated. Do not edit! Edit gherkin-javascript.razor instead.
import {
  ParserException,
  CompositeParserException,
  UnexpectedTokenException,
  UnexpectedEOFException,
  AstBuilderException,
  NoSuchLanguageException
} from './Errors'
import AstBuilder from './AstBuilder'
import Token from './Token'
import TokenScanner from './TokenScanner'
import TokenMatcher from './TokenMatcher'

export enum TokenType {
  None,
  @foreach(var rule in Model.RuleSet.TokenRules)
  {<text>  @rule.Name.Replace("#", ""),
</text>}
}

export enum RuleType {
  None,
  @foreach(var rule in Model.RuleSet.Where(r => !r.TempRule))
  {<text>  @rule.Name.Replace("#", "_"), // @rule.ToString(true)
</text>}
}

interface Context {
  tokenScanner: TokenScanner
  tokenMatcher: TokenMatcher
  tokenQueue: Token[]
  errors: Error[]
}

export default class Parser {
  public stopAtFirstError: boolean = false
  private context: Context

  constructor(private readonly builder: AstBuilder) {
  }

  public parse(tokenScanner: TokenScanner|string, tokenMatcher: TokenMatcher = new TokenMatcher()) {
    if(typeof tokenScanner === 'string') {
      tokenScanner = new TokenScanner(tokenScanner);
    }
    this.builder.reset();
    tokenMatcher.reset();
    this.context = {
      tokenScanner,
      tokenMatcher,
      tokenQueue: [],
      errors: []
    };
    this.startRule(this.context, RuleType.@Model.RuleSet.StartRule.Name);
    let state = 0;
    let token = null;
    while(true) {
      token = this.readToken(this.context);
      state = this.matchToken(state, token, this.context);
      if(token.isEof) break;
    }

    this.endRule(this.context);

    if(this.context.errors.length > 0) {
      throw CompositeParserException.create(this.context.errors);
    }

    return this.getResult();
  }

  private addError(context: Context, error: Error) {
    context.errors.push(error);
    if (context.errors.length > 10)
      throw CompositeParserException.create(context.errors);
  }

  private startRule(context: Context, ruleType: RuleType) {
    this.handleAstError(context, () => this.builder.startRule(ruleType));
  }

  private endRule(context: Context) {
    this.handleAstError(context, () => this.builder.endRule());
  }

  private build(context: Context, token: Token) {
    this.handleAstError(context, () => this.builder.build(token));
  }

  private getResult() {
    return this.builder.getResult();
  }

  private handleAstError(context: Context, action: () => any) {
    this.handleExternalError(context, true, action)
  }

  private handleExternalError<T>(context: Context, defaultValue: T, action: () => T) {
    if(this.stopAtFirstError) return action()
    try {
      return action()
    } catch (e) {
      if(e instanceof CompositeParserException) {
        e.errors.forEach((error: Error) => this.addError(context, error))
      } else if(
        e instanceof ParserException ||
        e instanceof AstBuilderException ||
        e instanceof UnexpectedTokenException ||
        e instanceof NoSuchLanguageException
      ) {
        this.addError(context, e)
      } else {
        throw e;
      }
    }
    return defaultValue;
  }

  private readToken(context: Context) {
    return context.tokenQueue.length > 0 ?
      context.tokenQueue.shift() :
      context.tokenScanner.read();
  }

  private matchToken(state: number, token: Token, context: Context) {
    switch(state) {
    @foreach(var state in Model.States.Values.Where(s => !s.IsEndState))
    {
    @:case @state.Id:
      @:return this.matchTokenAt_@(state.Id)(token, context);
    }
    default:
      throw new Error("Unknown state: " + state);
    }
  }

@foreach(var state in Model.States.Values.Where(s => !s.IsEndState))
{
<text>
  // @Raw(state.Comment)
  private matchTokenAt_@(state.Id)(token: Token, context: Context) {
    @foreach(var transition in state.Transitions)
    {
    @:if(this.@MatchToken(transition.TokenType)) {
      if (transition.LookAheadHint != null)
      {
      @:if(this.lookahead_@(transition.LookAheadHint.Id)(context, token)) {
      }
      foreach(var production in transition.Productions)
      {
        @CallProduction(production)
      }
      @:return @transition.TargetState;
      if (transition.LookAheadHint != null)
      {
      @:}
      }
    @:}
    }
    @HandleParserError(state.Transitions.Select(t => "#" + t.TokenType.ToString()).Distinct(), state)
  }
</text>
}

@foreach(var rule in Model.RuleSet.TokenRules)
{
<text>
  private match_@(rule.Name.Replace("#", ""))(context: Context, token: Token) {
    @if (rule.Name != "#EOF")
    {
    @:if(token.isEof) return false;
    }
    return this.handleExternalError(context, false, () => context.tokenMatcher.match_@(rule.Name.Replace("#", ""))(token));
  }
</text>
}

@foreach(var lookAheadHint in Model.RuleSet.LookAheadHints)
{
<text>
  private lookahead_@(lookAheadHint.Id)(context: Context, currentToken: Token) {
    currentToken.detach();
    let token;
    const queue: Token[] = [];
    let match = false;
    do {
      token = this.readToken(this.context);
      token.detach();
      queue.push(token);

      if (false @foreach(var tokenType in lookAheadHint.ExpectedTokens) { <text>|| this.@MatchToken(tokenType)</text>}) {
        match = true;
        break;
      }
    } while(false @foreach(var tokenType in lookAheadHint.Skip) { <text>|| this.@MatchToken(tokenType)</text>});

    context.tokenQueue = context.tokenQueue.concat(queue);

    return match;
  }
</text>
}

}
